package spitfire.ksim.simulator;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * <p>
 * KSimulator is an event-driven simulator engine. It keeps the current
 * simulation time and increases the time by one time tick every iteration. In
 * the end of each iteration, denoted as a tick here, the engine will sleep
 * <i>refresh period</i> in real time.
 * </p>
 * <p>
 * During every tick, jobs that added to the simulator are scanned, and those
 * whose schedule time is within the current time tick are dispatched to be
 * executed by their job executor.
 * </p>
 * <p>
 * <i> This part of behavior will be removed in the future.<br/>
 * The simulator also keeps a set of simulated objects. The initial jobs of the
 * simulated objects are added when the simulator is started. The simulation
 * duration is calculated and set at the same time. </i>
 * </p>
 * <p>
 * <b>This implementation is thread safe</b>
 * </p>
 * 
 * @see spitfire.ksim.simulator.KJob
 * @see spitfire.ksim.simulator.KSimObject
 * 
 * @author Adam
 * 
 */
public class KSimulator implements Runnable {

	public static final long DEFAULT_TIME_TICK = 60 * 1000;
	public static final long DEFAULT_REFRESH_PERIOD = 10;

	/** Current simulation time. */
	private long simTime = KSimDuration.UNDEFINED;
	/** Start time of the simulator. */
	private long startTime = KSimDuration.UNDEFINED;
	private KSimDuration simDuration = new KSimDuration();
	/** The increment of simulation time between every iteration. */
	private long timeTick = DEFAULT_TIME_TICK;
	/** The sleep time between every iteration. */
	private long refreshPeriod = DEFAULT_REFRESH_PERIOD;
	/** Whether the simulator is paused. */
	private boolean paused = false;
	/** Whether the simulator is stopped. */
	private boolean stopped = true;
	/** The thread that drives the simulator. */
	private Thread simThread;
	/** Simulated objects. <i>It will be removed in the future.</i> */
	private Set<KSimObject> simObjectSet = Collections
			.synchronizedSet(new HashSet<KSimObject>());
	/** Jobs to process in the next tick. */
	private Queue<KJob> jobQueue = new ConcurrentLinkedQueue<KJob>();
	/** Jobs to process in the current tick. */
	private Queue<KJob> execQueue = new ConcurrentLinkedQueue<KJob>();
	/** List of state change listeners. */
	private List<KStateChangeListener> scLstnrList = Collections
			.synchronizedList(new ArrayList<KStateChangeListener>());
	/** List of simulation time elapse listeners. */
	private List<KSimTimeElapseListener> teLstnrList = Collections
			.synchronizedList(new ArrayList<KSimTimeElapseListener>());
	/** List of destroy listeners. */
	private List<KOnDestroyListener> odLstnrList = Collections
			.synchronizedList(new ArrayList<KOnDestroyListener>());

	/**
	 * Construct an KSimulator instance with default time tick (60s) and default
	 * refresh period (10ms).
	 */
	public KSimulator() {
	}

	/**
	 * @return whether the simulator is stopped. The simulator is regarded as
	 *         stopped before it's started, or after it's stopped, or simulation
	 *         time goes to the end of simulation duration.
	 */
	public synchronized boolean isStopped() {
		return stopped;
	}

	private synchronized void setStopped(boolean stop) {
		stopped = stop;
	}

	/**
	 * @return whether the simulator is paused.
	 */
	public synchronized boolean isPaused() {
		return paused;
	}

	private synchronized void setPaused(boolean pause) {
		paused = pause;
	}

	/**
	 * @return current simulation time.
	 */
	public long getSimTime() {
		return simTime;
	}

	/**
	 * Set the simulation time to the given time.
	 * 
	 * @param time
	 *            - the simulation time to be set.
	 */
	public synchronized void setSimTime(long time) {
		simTime = time;
	}

	/**
	 * @return the minimum time among the simulated objects. <i>This method will
	 *         be removed in the future.</i>
	 */
	public long getFrontTime() {
		synchronized (simDuration) {
			return simDuration.getStartTime();
		}
	}

	/**
	 * @return the end of the simulation duration.
	 */
	public long getEndTime() {
		synchronized (simDuration) {
			return simDuration.getEndTime();
		}
	}

	/**
	 * @return the start of the simulation.
	 */
	public long getStartTime() {
		return startTime;
	}

	/**
	 * Extend the simulation duration to cover the given duration. <i>This
	 * method will be removed in the future.</i>
	 * 
	 * @param duration
	 *            - the duration to cover.
	 */
	public void coverDuration(KSimDuration duration) {
		synchronized (this.simDuration) {
			this.simDuration.coverDuration(duration);
		}
	}

	/**
	 * @return refresh period of the simulator.
	 */
	public synchronized long getRefreshPeriod() {
		return refreshPeriod;
	}

	/**
	 * Set the refresh period of the simulator to the given value.
	 * 
	 * @param period
	 *            - the refresh period to be set.
	 */
	public synchronized void setRefreshPeriod(long period) {
		refreshPeriod = period;
	}

	/**
	 * @return the time tick of the simulator.
	 */
	public synchronized long getTimeTick() {
		return timeTick;
	}

	/**
	 * Set the time tick of the simulator to the given value.
	 * 
	 * @param tick
	 *            - the time tick to be set.
	 */
	public synchronized void setTimeTick(long tick) {
		timeTick = tick;
	}

	/**
	 * Add a simulated object. <i>This method will be removed in the future.</i>
	 * 
	 * @param object
	 *            - the object to be added.
	 * @return <i>true</i> if this object hasn't be added before.
	 */
	public boolean addSimObject(KSimObject object) {
		return simObjectSet.add(object);
	}

	/**
	 * Remove a simulated object. <i>This method will be removed in the
	 * future.</i>
	 * 
	 * @param object
	 *            - the object to be removed.
	 * @return <i>true</i> if this object is simulated.
	 */
	public boolean removeSimObject(KSimObject object) {
		synchronized (object) {
			boolean res = simObjectSet.remove(object);
			removeJobOf(object);
			removeExecJobOf(object);
			return res;
		}
	}

	// -------------------Job Handler-------------------------------------------------
	/**
	 * Add a job to the simulator.
	 * 
	 * @param job
	 *            - the job to be added.
	 * @param executor
	 *            - the executor of the job.
	 * @throws NullPointerException
	 *             if the job executor is null.
	 */
	public void addJob(KJob job, KSimObject executor) {
		if (executor == null) {
			throw new NullPointerException("Job executor can't be null");
		}
		job.setJobExecutor(executor);
		if (job.getScheduleTime() == KJob.IMMEDIATE) {
			execQueue.add(job);
		} else {
			jobQueue.add(job);
		}
	}

	/**
	 * Remove the given job from the simulator.
	 * 
	 * @param job
	 *            - the job to be removed.
	 */
	public void removeJob(KJob job) {
		jobQueue.removeAll(Collections.singleton(job));
	}

	/**
	 * Remove all future jobs of the given simulated object.
	 * 
	 * @param jobExecutor
	 *            - executor of the jobs those are to be removed.
	 */
	public void removeJobOf(KSimObject jobExecutor) {
		Queue<KJob> tempQueue = new LinkedList<KJob>();
		while (!jobQueue.isEmpty()) {
			KJob job = (KJob) jobQueue.remove();
			if (!jobExecutor.equals(job.getJobExecutor())) {
				tempQueue.add(job);
			}
		}
		while (!tempQueue.isEmpty()) {
			jobQueue.add(tempQueue.remove());
		}
	}

	/**
	 * Remove all current jobs of the given simulated object.
	 * 
	 * @param jobExecutor
	 *            - executor of the jobs those are to be removed.
	 */
	public void removeExecJobOf(KSimObject jobDoer) {
		Queue<KJob> tempQueue = new LinkedList<KJob>();
		while (!execQueue.isEmpty()) {
			KJob job = (KJob) execQueue.remove();
			if (!jobDoer.equals(job.getJobExecutor())) {
				tempQueue.add(job);
			}
		}
		while (!tempQueue.isEmpty()) {
			execQueue.add(tempQueue.remove());
		}
	}

	// -----------------Control------------------------------------------------------
	/**
	 * Stop the simulator. If the simulator is already stopped, do nothing. This
	 * method will return after simulator is stopped.
	 */
	public void stop() {
		if (isStopped()) {
			return;
		}
		if (isPaused()) {
			resume();
		}
		setStopped(true);
		if (simThread != null && Thread.currentThread() != simThread) {
			try {
				simThread.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		invokeStateChange();
	}

	/**
	 * Pause the simulator. If the simulator hasn't started or already paused,
	 * do nothing. This method will return immediately and simulator will pause
	 * as soon as possible.
	 */
	public void pause() {
		if (!isPaused() && !isStopped()) {
			setPaused(true);
			invokeStateChange();
		}
	}

	/**
	 * Resume the simulator. If the simulator is not paused, do nothing.
	 */
	public void resume() {
		if (isPaused() && !isStopped()) {
			synchronized (this) {
				paused = false;
				notify();
			}
			invokeStateChange();
		}
	}

	// -----------------Thread related------------------------------------------------
	/**
	 * Start the simulation from the beginning. If it is already started, do
	 * nothing.
	 */
	public void start() {
		startFrom(simDuration.getStartTime());
	}

	/**
	 * Start the simulator from the given simulation time. If it is already 
	 * started, do nothing.
	 * 
	 * @param time - the time to start from.
	 */
	public void startFrom(long time) {
		if (!isStopped()) {
			return;
		}
		// adjust simulation simDuration, add Initial jobs
		synchronized (simObjectSet) {
			for (KSimObject simObject : simObjectSet) {
				// coverDuration(simObject.getSimDuration());
				if (simObject.getInitialJobs() != null) {
					jobQueue.addAll(simObject.getInitialJobs());
				}
			}
		}
		setSimTime(time);
		startTime = time;
		setStopped(false);
		invokeStateChange();
		simThread = new Thread(this);
		simThread.start();
	}

	/**
	 * Simulator job dispatching method. It usually shouldn't be invoked anywhere
	 * out of this class. To start the simulator, use {@link KSimulator#start()} 
	 * or {@link KSimulator#startFrom(long)}.
	 */
	@Override
	public void run() {
		DateFormat dateFormat = DateFormat.getDateTimeInstance();
		System.out.println("Simulation started at "
				+ dateFormat.format(getSimTime()));

		// Simulation started
		while (!stopped) {
			// System.out.println(dateFormat.format(getSimTime()) + jobQueue);
			// Move jobs from jobQueue to execQueue
			while (!jobQueue.isEmpty()) {
				execQueue.add(jobQueue.remove());
			}

			// Process the registered jobs
			while (!execQueue.isEmpty() && !stopped) {
				KSimObject obj = null;
				KJob job = execQueue.peek();
				if (job == null) {
					continue;
				}
				obj = job.getJobExecutor();
				synchronized (obj) {
					if (execQueue.poll() != job) {
						continue;
					}
					long scheduleTime = job.getScheduleTime();
					long interval = Math.abs(getSimTime() - scheduleTime);
					if (scheduleTime > 0 && interval > (getTimeTick() / 2)) {
						// Leave the job for future
						jobQueue.add(job);
					} else {
						// Process the job
						KSimObject jobDoer = job.getJobExecutor();
						jobDoer.doJob(job);
					}
				}
			}

			// Increase simulation time by timeTick
			setSimTime(getSimTime() + getTimeTick());
			invokeSimTimeElapsed();

			if (getSimTime() >= simDuration.getEndTime()) {
				stop();
			} else {
				// Sleep refreshPeriod to relax the CPU
				try {
					Thread.sleep(getRefreshPeriod());
					synchronized (this) {
						while (paused) {
							wait();
						}
					}
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
		jobQueue.clear();
		execQueue.clear();
		Date endDate = new Date(getSimTime());
		System.out.println("Simulation done at " + dateFormat.format(endDate));
	}

	/**
	 * Add the given state change listener.
	 * 
	 * @param l - the state change listener to be added.
	 */
	public void addStateChangeListener(KStateChangeListener l) {
		scLstnrList.add(l);
	}

	/**
	 * Remove the given state change listener.
	 * 
	 * @param l - the state change listener to be Removed.
	 */
	public void removeStateChangeListener(KStateChangeListener l) {
		scLstnrList.remove(l);
	}

	/**
	 * Remove all state change listeners.
	 */
	public void removeAllStateChangeListener() {
		scLstnrList.clear();
	}

	private void invokeStateChange() {
		synchronized (scLstnrList) {
			for (KStateChangeListener l : scLstnrList) {
				l.stateChanged();
			}
		}
	}

	public void addSimTimeElapseListener(KSimTimeElapseListener l) {
		teLstnrList.add(l);
	}

	public void removeSimTimeElapseListener(KSimTimeElapseListener l) {
		teLstnrList.remove(l);
	}

	public void removeAllSimTimeElapseListener() {
		teLstnrList.clear();
	}

	private void invokeSimTimeElapsed() {
		synchronized (teLstnrList) {
			for (KSimTimeElapseListener l : teLstnrList) {
				l.timeElapsed();
			}
		}
	}

	public void addOnDestroyListener(KOnDestroyListener l) {
		odLstnrList.add(l);
	}

	public void removeOnDestroyListener(KOnDestroyListener l) {
		odLstnrList.remove(l);
	}

	public void removeAllOnDestroyListener() {
		odLstnrList.clear();
	}

	private void invokeOnDestroy() {
		synchronized (odLstnrList) {
			for (KOnDestroyListener l : odLstnrList) {
				l.onDestroy();
			}
		}
	}

	/**
	 * Stop the simulator, clear all listeners, and detach all simObject in 
	 * this simulator. But NOT destroy simObjects.
	 * <i></i>
	 */
	public void destroy() {
		synchronized (this) {
			if (!isStopped()) {
				stop();
			}
			Collection<KSimObject> simObjCopies = new ArrayList<KSimObject>(
					simObjectSet);
			for (KSimObject kSimObject : simObjCopies) {
				kSimObject.detachFromSimulator();
			}
		}
		removeAllStateChangeListener();
		removeAllSimTimeElapseListener();
		KJob.resetID();
		invokeOnDestroy();
		removeAllOnDestroyListener();
	}

	public void printJobQueue() {
		System.out.println(jobQueue);
	}

	public void printExecQueue() {
		System.out.println(execQueue);
	}
}
